{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "# YOLO v1总结\n",
    "## 预测结果\n",
    "输入[-1, 3, 448, 448]，输出[7, 7, 30]\n",
    "\n",
    "其中30包括：\n",
    "### 2个框的位置(x,y,w,h)---8个\n",
    "1. x和y要乘以cell_size才能得到真实坐标，所以中心点一定在这个cell里面\n",
    "\n",
    "$b_x=\\sigma(t_x)+c_x\\\\b_y=\\sigma(t_y)+c_y\\\\b_w=p_we^{t_w}\\\\b_h=p_he^{t_h}$\n",
    "### 2个bbox置信度\n",
    "1. bbox的置信度等于该bbox内存在物体的概率x该bbox与该对象实际bbox的IOU\n",
    "2. 综合来说，一个bbox的置信度包含了：bbox内是否包含对象，且对象位置是否准确\n",
    "3. 该IOU是在训练过程计算的，inference时无需计算\n",
    "\n",
    "$Confidence=Pr(Object)*IOU_{pred}^{truth}$\n",
    "### 20个类别条件概率\n",
    "$Pr(Class_i|Object)*Pr(Object)*IOU_{pred}^{truth}$\n",
    "\n",
    "等式左边第一项时是个网格预测的类别信息（条件概率：当存在物体时），第二三项分别是每个bbox\n",
    "\n",
    "## 构建训练样本\n",
    "一个物体中心出现在该网格内时，该网格预测的2个bbox会同真实bbox求IOU，谁的IOU大，谁就负责该物体的预测，因此该cell的标签中，一个bbox的置信度为1，一个为0\n",
    "1. 只负责预测的bbox才计入的损失：真实目标在该网格内，且该bbox与标签物体的IOU比该网格另外一个bbox的大\n",
    "    1. 存在目标的bbox的边框中心误差：权重5\n",
    "    2. 存在目标的bbox的边宽高度和宽度的误差：权重5，约束$\\sqrt{w}$和$\\sqrt{h}$，直接取差值的话，大的对象对差值的敏感度较低，小的对象对差值的敏感度较高，所以取平方根可以降低这种敏感度的差异，使得较大的对象和较小的对象在尺寸误差上有相似的权重。\n",
    "    3. 存在对象的bbox的置信度误差：置信度为$Confidence=Pr(Object)*IOU_{pred}^{truth}$\n",
    "    4. 对象类别误差\n",
    "2. 不负责预测的bbox的误差：\n",
    "    1. 置信度误差（期待该置信度为0）：权重0.5\n",
    "## NMS\n",
    "1. 将所有bbox放入队列根据$Pr(Class_i|Object)*Pr(Object)$排序\n",
    "2. 每次选择$Pr(Class_i|Object)*Pr(Object)$最高的bbox，再将队列中其余bbox与其IOU超过阈值的bbox剔除\n",
    "3. 重复进行。\n",
    "## 优点\n",
    "1. 采用一个CNN\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "source": [
    "import torch\n",
    "import math\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.utils.model_zoo as model_zoo\n",
    "import cv2\n",
    "import numpy as np\n",
    "import torchvision.transforms as transforms\n",
    "from torch.autograd import Variable"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "# 网络结构"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 权重下载"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "source": [
    "model_urls = {\n",
    "    'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',\n",
    "    'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',\n",
    "    'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',\n",
    "    'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',\n",
    "    'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',\n",
    "}\n",
    "\n",
    "VOC_CLASSES = (    # always index 0\n",
    "    'aeroplane', 'bicycle', 'bird', 'boat',\n",
    "    'bottle', 'bus', 'car', 'cat', 'chair',\n",
    "    'cow', 'diningtable', 'dog', 'horse',\n",
    "    'motorbike', 'person', 'pottedplant',\n",
    "'sheep', 'sofa', 'train', 'tvmonitor')\n",
    "\n",
    "Color = [[0, 0, 0],\n",
    "        [128, 0, 0],\n",
    "        [0, 128, 0],\n",
    "        [128, 128, 0],\n",
    "        [0, 0, 128],\n",
    "        [128, 0, 128],\n",
    "        [0, 128, 128],\n",
    "        [128, 128, 128],\n",
    "        [64, 0, 0],\n",
    "        [192, 0, 0],\n",
    "        [64, 128, 0],\n",
    "        [192, 128, 0],\n",
    "        [64, 0, 128],\n",
    "        [192, 0, 128],\n",
    "        [64, 128, 128],\n",
    "        [192, 128, 128],\n",
    "        [0, 64, 0],\n",
    "        [128, 64, 0],\n",
    "        [0, 192, 0],\n",
    "        [128, 192, 0],\n",
    "        [0, 64, 128]]"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 骨干网路--ResNet50"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### Bottle-neck块\n",
    "conv-bn-relu(inplanes, planes, stride=1)\n",
    "\n",
    "conv-bn-relu(planes, planes, stride=stride)\n",
    "\n",
    "conv-bn(planes, planes*4, stride=1)"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "source": [
    "class Bottleneck(nn.Module):\n",
    "    \"\"\"\n",
    "    输入：\n",
    "        inplanes：输入通道数；\n",
    "        planes：中间层通道数\n",
    "        downsample：用于将原本输入通道数inplanes改为planes*4\n",
    "    输出：\n",
    "        通道数：planes*4\n",
    "    \"\"\"\n",
    "    expansion = 4\n",
    "\n",
    "    def __init__(self, inplanes, planes, stride=1, downsample=None):\n",
    "        super(Bottleneck, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)\n",
    "        self.bn1 = nn.BatchNorm2d(planes)\n",
    "\n",
    "        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,\n",
    "                               padding=1, bias=False)\n",
    "        self.bn2 = nn.BatchNorm2d(planes)\n",
    "\n",
    "        self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)\n",
    "        self.bn3 = nn.BatchNorm2d(planes * 4)\n",
    "        self.relu = nn.LeakyReLU(inplace=True)\n",
    "        \n",
    "        self.downsample = downsample\n",
    "        self.stride = stride\n",
    "\n",
    "    def forward(self, x):\n",
    "        residual = x\n",
    "\n",
    "        out = self.conv1(x)\n",
    "        out = self.bn1(out)\n",
    "        out = self.relu(out)\n",
    "\n",
    "        out = self.conv2(out)\n",
    "        out = self.bn2(out)\n",
    "        out = self.relu(out)\n",
    "\n",
    "        out = self.conv3(out)\n",
    "        out = self.bn3(out)\n",
    "\n",
    "        if self.downsample is not None:\n",
    "            residual = self.downsample(x)\n",
    "\n",
    "        out += residual\n",
    "        out = self.relu(out)\n",
    "\n",
    "        return out"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### detnet-bottleneck\n",
    "相比于bottle neck没有expansion，但是中间层有dilation"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "source": [
    "class detnet_bottleneck(nn.Module):\n",
    "    expansion = 1\n",
    "    def __init__(self, in_planes, planes, stride=1, block_type='A'):\n",
    "        super(detnet_bottleneck, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)\n",
    "        self.bn1 = nn.BatchNorm2d(planes)\n",
    "        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=2, bias=False,dilation=2)\n",
    "        self.bn2 = nn.BatchNorm2d(planes)\n",
    "        self.conv3 = nn.Conv2d(planes, self.expansion*planes, kernel_size=1, bias=False)\n",
    "        self.bn3 = nn.BatchNorm2d(self.expansion*planes)\n",
    "\n",
    "        self.downsample = nn.Sequential()\n",
    "        if stride != 1 or in_planes != self.expansion*planes or block_type=='B':\n",
    "            self.downsample = nn.Sequential(\n",
    "                nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),\n",
    "                nn.BatchNorm2d(self.expansion*planes)\n",
    "            )\n",
    "\n",
    "    def forward(self, x):\n",
    "        out = F.relu(self.bn1(self.conv1(x)))\n",
    "        out = F.relu(self.bn2(self.conv2(out)))\n",
    "        out = self.bn3(self.conv3(out))\n",
    "        out += self.downsample(x)\n",
    "        out = F.relu(out)\n",
    "        return out\n",
    "\n"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### ResNet\n",
    "卷积计算代大小：$o=\\lfloor \\frac{n+2p-f}{s} \\rfloor + 1$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "source": [
    "class ResNet(nn.Module):\n",
    "    \"\"\"\n",
    "    输入：\n",
    "        Tensor:[n, 3, 448, 448]\n",
    "    输出：\n",
    "        Tensor:[n, 14, 14, 30]\n",
    "    \"\"\"\n",
    "    def __init__(self, block, layers=[3, 4, 6, 3], num_classes=1470):\n",
    "        self.inplanes = 64  #每个layer的起始输入通道数\n",
    "        super(ResNet, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)\n",
    "        self.bn1 = nn.BatchNorm2d(64)\n",
    "        self.relu = nn.LeakyReLU(inplace=True)\n",
    "        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n",
    "        self.layer1 = self._make_layer(block, 64, layers[0])\n",
    "        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)\n",
    "        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)\n",
    "        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)\n",
    "        self.layer5 = self._make_layer(block, 512, layers[3], stride=2)\n",
    "        self.avgpool = nn.AvgPool2d(7) ##7x7x2048---1x1x2048\n",
    "        self.fc = nn.Linear(512*block.expansion, num_classes)\n",
    "        self.bn_end = nn.BatchNorm2d(num_classes)\n",
    "        #self.layer5 = self._make_detnet_layer(in_channels=2048)\n",
    "\n",
    "        # self.conv_end = nn.Conv2d(256, 30, kernel_size=3, stride=1, padding=1, bias=False)\n",
    "        # self.bn_end = nn.BatchNorm2d(30)\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Conv2d):\n",
    "                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels\n",
    "                m.weight.data.normal_(0, math.sqrt(2. / n))\n",
    "            elif isinstance(m, nn.BatchNorm2d):\n",
    "                m.weight.data.fill_(1)  #初始化scale\n",
    "                m.bias.data.zero_()     #初始化bias\n",
    "\n",
    "\n",
    "    def _make_layer(self, block, planes, blocks, stride=1):\n",
    "        \"\"\"描述：\n",
    "            创建blocks个bottleneck，其中只有第一个进行stride\n",
    "            其他都是1x1降维度,3x3卷积，1x1升维\n",
    "        输出：\n",
    "            通道数为planes*4\n",
    "        \"\"\"\n",
    "        downsample = None\n",
    "        ## Bottleneck结构需要将输入和输出相加，当stride!=1时，尺寸对应不上\n",
    "        # 当inplanes...时,通道数对应不上，因此downsamples用于调整，使之能相加\n",
    "        if stride != 1 or self.inplanes != planes * block.expansion:\n",
    "            downsample = nn.Sequential(\n",
    "                nn.Conv2d(self.inplanes, planes * block.expansion,\n",
    "                          kernel_size=1, stride=stride, bias=False),\n",
    "                nn.BatchNorm2d(planes * block.expansion),\n",
    "            )\n",
    "        layers = []\n",
    "        layers.append(block(self.inplanes, planes, stride, downsample))\n",
    "        self.inplanes = planes * block.expansion\n",
    "        for i in range(1, blocks):\n",
    "            layers.append(block(self.inplanes, planes))\n",
    "\n",
    "        return nn.Sequential(*layers)\n",
    "\n",
    "    def _make_detnet_layer(self, in_channels):\n",
    "        layers = []\n",
    "        layers.append(detnet_bottleneck(in_planes=in_channels, planes=256, block_type='B'))\n",
    "        layers.append(detnet_bottleneck(in_planes=256, planes=256, block_type='A'))\n",
    "        layers.append(detnet_bottleneck(in_planes=256, planes=256, block_type='A'))\n",
    "        return nn.Sequential(*layers)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = self.conv1(x)\n",
    "        x = self.bn1(x)\n",
    "        x = self.relu(x)\n",
    "        x = self.maxpool(x)\n",
    "\n",
    "        x = self.layer1(x)\n",
    "        x = self.layer2(x)\n",
    "        x = self.layer3(x)\n",
    "        x = self.layer4(x)\n",
    "        x = self.layer5(x)\n",
    "        x = self.avgpool(x)\n",
    "        x = x.view(x.size(0), -1)\n",
    "        x = self.fc(x)\n",
    "        # print(x.shape)\n",
    "        # x = self.bn_end(x)\n",
    "        x = torch.sigmoid(x)\n",
    "        x = x.view(-1, 7, 7, 30)\n",
    "        # x = self.conv_end(x)\n",
    "        # x = self.bn_end(x)\n",
    "        #x = torch.sigmoid(x)    #归一化到0-1\n",
    "        #x = x.permute(0, 2, 3, 1) #将[batch, c, w, h]变为[batch, w, h, c]，[-1, 14, 14, 30]\n",
    "        return x\n",
    "\n",
    "def resnet50(pretrained=False, **kwargs):\n",
    "    model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)\n",
    "    if pretrained:\n",
    "        model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))\n",
    "    return model"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## YOLO v1"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 网络细节\n",
    "resnet输出结果为14x14x30，其中30分别为：x1,y1,w1,h1,contain_prob1, x2,y2,w2,h2,contain_prob2,c1_prob,c2_...\n",
    "### 过滤\n",
    "1. contain_prob排除\n",
    "2. contain_prob * max_cls_prob排除一次，最后的概率也是contain*cls\n",
    "3. nms排除一次\n",
    "### nms过程\n",
    "\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "source": [
    "class YOLOv1:\n",
    "    def __init__(self) -> None:\n",
    "        self.resnet = resnet50().cuda()\n",
    "    def predict_gpu(self, image_name, root_path=''):\n",
    "        self.resnet.eval()\n",
    "        result = []\n",
    "        image = cv2.imread(root_path + image_name)\n",
    "        image_show = image\n",
    "        h, w, _ = image.shape\n",
    "        img = cv2.resize(image, (448, 448))\n",
    "        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n",
    "        mean = (123, 117, 104) #RGB\n",
    "        img = img - np.array(mean, dtype=np.float32)\n",
    "        transform = transforms.Compose([transforms.ToTensor(),])\n",
    "        img = transform(img)\n",
    "        img = Variable(img[None,:,:,:], requires_grad=False)\n",
    "        print(\"Input size:\",img.shape)\n",
    "        img = img.cuda()\n",
    "        pred = self.resnet(img)\n",
    "        pred = pred.cpu()\n",
    "        boxes,cls_indexs,probs =  self.decoder(pred)\n",
    "\n",
    "        for i, box in enumerate(boxes):\n",
    "            x1 = int(box[0]*w)\n",
    "            x2 = int(box[2]*w)\n",
    "            y1 = int(box[1]*h)\n",
    "            y2 = int(box[3]*h)\n",
    "            cls_index = cls_indexs[i]\n",
    "            cls_index = int(cls_index) # convert LongTensor to int\n",
    "            prob = probs[i]\n",
    "            prob = float(prob)\n",
    "            result.append([(x1,y1),(x2,y2),VOC_CLASSES[cls_index],image_name,prob])\n",
    "        for left_up,right_bottom,class_name,_,prob in result:\n",
    "            color = Color[VOC_CLASSES.index(class_name)]\n",
    "            cv2.rectangle(image,left_up,right_bottom,color,2)\n",
    "            label = class_name+str(round(prob,2))\n",
    "            text_size, baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)\n",
    "            p1 = (left_up[0], left_up[1]- text_size[1])\n",
    "            cv2.rectangle(image, (p1[0] - 2//2, p1[1] - 2 - baseline), (p1[0] + text_size[0], p1[1] + text_size[1]), color, -1)\n",
    "            cv2.putText(image, label, (p1[0], p1[1] + baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1, 8)\n",
    "\n",
    "            cv2.imwrite('v1-result.jpg',image)\n",
    "            \n",
    "\n",
    "    def decoder(self, pred):\n",
    "        grid_num = 7\n",
    "        boxes = []\n",
    "        cls_indexs = []\n",
    "        probs = []\n",
    "        cell_size = 1./grid_num #假设整张图片尺寸为1x1，则每个cell大小为:1./14\n",
    "        pred = pred.data                        #30张14x14，一次是[x1,y1,w1,h1,概率1,x2,y2,w2,h2,概率2,cl1,cl2,...]\n",
    "        pred = pred.squeeze(0)                  #[1, 14, 14, 30]->[14,14,30]\n",
    "        contain1 = pred[:,:,4].unsqueeze(2)      #[14,14]-->[14,14,1]\n",
    "        contain2 = pred[:,:,9].unsqueeze(2)\n",
    "        contain = torch.cat((contain1,contain2),2) #[14,14,1]+[14,14,1]=[14,14,2]\n",
    "        mask1 = contain > 0.1\n",
    "        mask2 = (contain==contain.max())\n",
    "        mask = (mask1 + mask2).gt(0)        #就算全部小于0.1也要保存最大值\n",
    "        for i in range(grid_num):\n",
    "            for j in range(grid_num):\n",
    "                for b in range(2):\n",
    "                    if mask[i, j, b] == 1:\n",
    "                        box = pred[i, j, b*5:b*5+4] #保存坐标\n",
    "                        contain_prob = torch.FloatTensor([pred[i, j, b*5+4]])\n",
    "                        xy = torch.FloatTensor([j,i]) * cell_size   #当前cell左上角坐标,ji是因为j对应的是x轴\n",
    "                        box[:2] = box[:2]*cell_size + xy\n",
    "                        box_xy = torch.FloatTensor(box.size())      #[x,y,w,h]--->[x1,y1,x2,y2]\n",
    "                        box_xy[:2] = box[:2] - 0.5*box[2:]\n",
    "                        box_xy[2:] = box[:2] + 0.5*box[2:]\n",
    "                        max_prob, cls_index = torch.max(pred[i, j,10:], 0)\n",
    "                        if float((contain_prob * max_prob)[0]) > 0.1:\n",
    "                            boxes.append(box_xy.view(1,4))\n",
    "                            cls_indexs.append(torch.Tensor([cls_index]))\n",
    "                            probs.append(contain_prob*max_prob)\n",
    "        # print(cls_indexs)\n",
    "        \n",
    "        if len(boxes) == 0:\n",
    "            boxes = torch.zeros((1,4))\n",
    "            probs = torch.zeros(1)\n",
    "            cls_indexs = torch.zeros(1)\n",
    "        else:\n",
    "            boxes = torch.cat(boxes, 0)\n",
    "            probs = torch.cat(probs, 0)\n",
    "            cls_indexs = torch.cat(cls_indexs,0)\n",
    "            # boxes = torch.Tensor(boxes)\n",
    "            # probs = torch.Tensor(probs)\n",
    "            # cls_indexs = torch.Tensor(cls_indexs)\n",
    "\n",
    "        keep = self.nms(boxes, probs)\n",
    "        return boxes[keep], cls_indexs[keep], probs[keep]\n",
    "\n",
    "    def nms(self, bboxes, scores, threshold=0.5):\n",
    "        x1 = bboxes[:,0]\n",
    "        y1 = bboxes[:,1]\n",
    "        x2 = bboxes[:,2]\n",
    "        y2 = bboxes[:,3]\n",
    "        areas = (x2-x1) * (y2-y1)\n",
    "        _,order = scores.sort(0,descending=True)\n",
    "        \n",
    "        keep = []\n",
    "        while order.numel() > 1:\n",
    "            i = order[0]    #最大概率的bbox的序号\n",
    "            keep.append(i)\n",
    "            if order.numel() == 1:\n",
    "                break\n",
    "\n",
    "            xx1 = x1[order[1:]].clamp(min=x1[i])\n",
    "            yy1 = y1[order[1:]].clamp(min=y1[i])\n",
    "            xx2 = x2[order[1:]].clamp(max=x2[i])\n",
    "            yy2 = y2[order[1:]].clamp(max=y2[i])\n",
    "\n",
    "            w = (xx2-xx1).clamp(min=0)\n",
    "            h = (yy2-yy1).clamp(min=0)\n",
    "            inter = w*h\n",
    "\n",
    "            IoUs = inter / (areas[i] + areas[order[1:]] - inter)\n",
    "            ids = (IoUs <= threshold).nonzero().squeeze()\n",
    "            if ids.numel() == 0:\n",
    "                break\n",
    "            order = order[ids+1]\n",
    "        return torch.LongTensor(keep)"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "source": [
    "yolov1 = YOLOv1()\n",
    "yolov1.predict_gpu(\"person.jpg\", \"/home/yinzp/gitee/ObjectDetection-2D/image/\")"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "(424, 640, 3)\n",
      "(448, 448, 3)\n",
      "(448, 448, 3)\n",
      "torch.Size([3, 448, 448])\n",
      "Input size: torch.Size([1, 3, 448, 448])\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [],
   "outputs": [],
   "metadata": {}
  }
 ],
 "metadata": {
  "orig_nbformat": 4,
  "language_info": {
   "name": "python",
   "version": "3.6.9",
   "mimetype": "text/x-python",
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "pygments_lexer": "ipython3",
   "nbconvert_exporter": "python",
   "file_extension": ".py"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3.6.9 64-bit"
  },
  "interpreter": {
   "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}